Глава 7. Объектно-ориентированное программирование

В этой главе мы приступаем, наконец, к самому существенному из того, ради чего писалась эта книга и ради чего, будем надеяться, вы ее читаете. Прежде чем заняться конкретным изучением классов C++, нам хотелось бы немного поговорить об объектно-ориентированном программировании вообще. Конечно, для читателей, которые уже знакомы с его концепциями, следующий раздел будет неинтересен, за что мы заранее извиняемся.

Об объектном подходе к программированию

Существует разные подходы к программированию. Любому из них присущ свой собственный способ абстрагирования сущностей, с которыми он работает. Так, процедурно-ориентированный подход оперирует абстракцией алгоритма. С, кстати, — типичный процедурный язык, хотя на нем возможно писать программы, напоминающие по стилю объектно-ориентированные. Логико-ориентированный имеет в виду цели, выражаемые на языке логических предикатов (таков язык Prolog). Наконец, объектно-ориентированное программирование абстрагирует классы и объекты. В чем же состоит его суть?

Известный теоретик Грейди Буч так определяет этот подход:

“Объектно-ориентированное программирование — это методология программирования, основанная на организации программы, в виде совокупности объектов, каждый из которых является представителем определенного класса, а классы образуют иерархию наследования.”

В “строго” объектно-ориентированных языках объектами (или их составными частями) являются решительно все элементы программы, в том числе она сама. В языке Java, например. Язык C++ таковым, кстати, не является хотя бы потому, что он сохраняет все возможности процедурного языка С. В C++ можно создать, например, совершенно отдельно стоящую глобальную переменную, да и сама функция main)) — совершенно “внеклассовая”. Такая универсальность C++ может быть как преимуществом, так и недостатком, если ею злоупотреблять. У программиста, впервые применяющего объектный подход, всегда имеется тенденция мыслить старыми, процедурными категориями.

Итак, центральным элементом абстракции объектно-ориентированной методологии является, очевидно, объект.

Объект

Объекты нашего программного мира моделируют объекты реального или воображаемого мира (например, мира некоторой игры, хотя это мир тоже мыслимый, а стало быть, реальный в смысле логики). Как модель, программный объект представляет собой некоторую абстракцию объекта реального, что предполагает выделение существенных свойств последнего и игнорирование тех, что безразличны с насущной, “сиюминутной”, точки зрения.

Как нечто, как единичность, объект (реальный или программный) отличен от всего остального. Он обладает индивидуальностью, или самотождественностью, если применить опять же термин логики. Понятие индивидуальности программного объекта определить сложно, пожалуй, даже сложнее, чем для реальных объектов, и о нем редко вообще задумываются. Однако интуитивно ясно, что объект, как бы его ни переименовывать и как бы ни менялось его состояние, остается той же самой единичной сущностью с момента создания и до своего уничтожения.

Наконец, заметим, что о программном объекте можно говорить точно так же, как о реальном. Мы можем не только говорить о нем, но и описывать на специальном формальном языке — языке программирования.

В общем, как видно, объект в программе мало чем отличается от предмета реального мира. Он является моделью последнего. Поэтому, в известном смысле, пока не важно, о каких объектах пойдет речь — реальных или программных.

Состояние объекта

Объект, прежде всего, характеризуется своим состоянием. Возьмем, к примеру, базу данных. Она — объект, поскольку есть нечто цельное. К числу моментов, определяющих ее состояние, относится текущее число записей. Каждая запись тоже, очевидно, является объектом. Отдельные поля существующей записи могут модифицироваться, и их совокупность определяет текущее состояние записи, а вместе с ней и состояние всей базы данных.

По-видимому, состояние программного объекта полностью определяется некотором набором (структурой) характеристик и их текущими значениями. Эти характеристики называют полями или (в C++) элементами данных объекта. Но не все моменты состояния объекта должны быть непосредственно видимы извне. Классический пример, который неизменно приводят американские авторы, — автомобиль. С точки зрения водителя у него есть приборный щиток, отражающий скорость, обороты, температуру двигателя и т. д., и органы управления. Но в автомобиле масса частей, спрятанных под капотом, состояние которых не исчерпывается показаниями приборов.

На самом деле состояние, как таковое, может быть вообще скрыто от внешнего взгляда. Оно, как говорят, инкапсулировано в объекте. Однако в зависимости от своего состояния объект по-разному взаимодействует со своим окружением, что приводит нас к следующему понятию.

Поведение объекта

Поведение — это то, как объект взаимодействует с окружением (другими объектами). Объект может подвергаться воздействию окружения или сам воздействовать на него. Объект может рассматриваться как аналог предмета, а поведение — как реакция на манипуляции с ним или действия, инициированные самим объектом. В некоторых объектных системах (например, OLE 2) говорят о глаголах, т. е. действиях, которые могут связываться с объектом. Для каждого объекта существует определенный набор возможных действий над ним.

Действия в отношении к объектам иногда называют передачей сообщений между ними. В языках, подобных Object Pascal, операции над объектами называют обычно методами. В C++ благодаря его “процедурному наследству” чаще говорят о функциях-элементах объекта. Эти функции являются структурными элементами определения класса, к которому принадлежит объект.

Отношения между объектами

Естественно, программа, т. е. цельная система, реализуется только во взаимодействии всех ее объектов. Здесь можно выделить в основном два типа взаимодействий, или отношений: связь и агрегацию.

Связь является довольно очевидной разновидностью взаимодействий — один объект может воздействовать на другой, являющийся в известном смысле автономной сущностью. Тут существует отношение подчинения — “А использует В”. Один объект является активным, другой — пассивным. Понятно, что в системе один и тот же объект может выступать как в активной, так и в пассивной роли по отношению к различным объектам.

Другой тип отношений — агрегация, когда один объект является составной частью, т. е. элементом другого — “А содержит В”. Агрегация может означать физическое вхождение одного объекта в другой; в C++ это соответствует описанию первого объекта в качестве элемента данных другого объекта. Но это не обязательно. Например, в Windows есть понятие дочернего окна. Здесь имеет место отношение агрегации, хотя на физическом уровне родительское и дочернее окна автономны.

Класс

Класс — это множество объектов, имеющих одинаковую структуру. Класс в программировании является аналогом понятия, или категории. В то время как объект представляет собой конкретную сущность, класс является абстракцией сущности объекта. Конкретный объект является представителем, или (не совсем грамотно) экземпляром класса.

Другими словами, структура характеристик объекта и все потенциальные отношения между объектами заложены в классе. Однако классы могут находиться еще и в специфических отношениях между собой.

Наследование

Если существенными видами отношений между объектами являются связь и агрегация, то фундаментальное отношение между классами — это наследование. Один класс может наследовать другому. В C++ в таком случае говорят, что один класс является базовым, а другой (который наследует первому) — производным. Еще их называют соответственно классом-предком и классом-потомком. Наследование может быть прямым, когда один класс является непосредственным предком (потомком) другого, или косвенным, когда имеют место промежуточные наследования.

Производный класс наследует всю структуру характеристик и поведение базового, однако может дополнять или модифицировать их. Если класс В является производным по отношению к А, то с логической точки зрения “В есть А”. Например, понятие, или класс, “автомобиль” (В) является производным по отношению к понятию “средство передвижения” (А). По Л. Н. Толстому, В есть А.

Как и в логике, здесь существует взаимосвязь между “содержанием” и “объемом” понятия. Производный класс имеет большее содержание, но меньший объем, чем базовый.

Наследование может быть простым, когда производный класс имеет всего одного непосредственного предка, и сложным, если в наследовании участвуют несколько базовых классов.

Полиморфизм

Полиморфизм, наряду с наследованием, является фундаментальной концепцией объектной модели программирования. Без него объектно-ориентированное программирование потеряло бы значительную долю своего смысла. Суть полиморфизма в том, что с объектами различных классов, имеющих один и тот же базовый класс, можно при определенных условиях обращаться, как с объектами базового класса; однако объект, являющийся, по видимости, объектом базового класса, будет вести себя по-разному в зависимости от того, что он такое на самом деле, т. е. представитель какого из производных классов.

В C++ полиморфное поведение объектов обеспечивается механизмом виртуальных функций-элементов. В одной из предыдущих глав мы на самом деле уже приводили пример “полиморфизма”, реализованного на языке С. Допустим, программа должна в числе всего прочего выводить на экран различные геометрические фигуры. Она определяет класс “фигура”, в котором предусмотрен виртуальный метод “нарисовать” (в C++ это был бы абстрактный класс). От данного класса можно произвести несколько классов — “точка”, “линия”, “круг” и т. д., — каждый из которых будет по-своему определять этот метод.

Указатель на класс “фигура” может ссылаться на объект любого из производных классов (поскольку все они являются фигурами), и для указываемого им объекта можно вызвать метод “нарисовать”, не имея представления о том, что это на самом деле за фигура.

Абстракция и инкапсуляция

Об этих принципах объектного подхода мы уже упоминали. На самом деле это принципы программирования, присущие не только объектно-ориентированной модели. Но хотелось бы несколько уточнить их в отношении к организации классов.

Чтобы абстрагировать объект, он должен быть сравнительно “слабо связан” с окружающим миром. Он должен обладать сравнительно небольшим набором (существенных) свойств, характеризующих его отношения с другими объектами. С другой стороны, выделение класса как некоторого понятия, охватывающего целый ряд различных объектов, также является моментом абстракции.

Поэтому абстрагирование, как таковое, имеет два аспекта: выделение общих и в тоже время существенных свойств, описывающих поведение ряда схожих предметов.

С абстракцией неразрывно связан принцип инкапсуляции. Инкапсуляция — это сокрытие второстепенных деталей объекта. Для этого нужно выделить сначала существенные его свойства. Но чтобы выделить существенные свойства, нужно сначала отвлечься от второстепенных. Так что в действительности речь может идти только о едином акте, в котором можно лишь отвлеченно выделить два отдельных момента.

С технической точки зрения абстракция и инкапсуляция выражаются в том, что классы состоят из интерфейса и реализации. Интерфейс представляет абстрагированную сущность объектов. Реализация скрыта в своих деталях от пользователя класса.

Сделав такие предварительные замечания о понятиях объектно-ориентированного подхода, перейдем к конкретному рассмотрению классов, как они реализованы в языке C++.

Введение в классы С++

Сначала мы приведем простейший пример определений классов, которые можно было бы разместить в заголовочном файле. Так обычно и делается, если классы должны быть доступны нескольким модулям программы. Следует напомнить, что класс — это расширение понятия типа данных, а точнее, понятия структуры. В C++ принято говорить просто о типах; представитель класса уже нельзя считать просто данными, поскольку ему присуще некоторое поведение.

После этого мы, опираясь на показанный пример, объясним элементарные моменты строения класса в C++.

Определение класса

Приведенный ниже код определяет два класса, которые могли бы применяться в графической программе. Это классы точек и линий.

////////////////////////////////////////////////

// Classesl.h: Пример двух геометрических классов. //

const int MaxX = 200; // Максимальные значения координат.

const int MaxY = 200;

//

struct,Point { // Класс точек.

int GetX(void) ;

int GetY(void) ;

void SetPoint(int, int);

private:

int x;

int y;

};

class Line

{

// Класс линий.

Point p0;

Point p1;

public:

Line(int x0, int y0, int xl, int yl);

// Конструктор.

~Line(void); // Деструктор.

void Show(void);

};

Ну вот, такие вот классы. Теперь разберем различные моменты этих определений.

Иногда может потребоваться предварительное объявление класса, если нужно, например, объявить указатель на объект класса прежде, чем будет определен сам класс. Предварительное объявление в этом смысле подобно прототипу функции и выглядит так:

class SomeClass;

Заголовок определения

Определение класса начинается с ключевых слов class, struct или union. Правда, union применяется крайне редко. И структуры, и классы, и объединения относятся к “классовым” типам C++. Разницу между этими типами мы рассмотрим чуть позже.

Спецификации доступа

Ключевые слова private и public называются спецификаторами доступа. Спецификатор private означает, что элементы данных и элементы-функции, размещенные под ним, доступны только функциям-элементам данного класса. Это так называемый закрытый доступ.

Спецификатор public означает, что размещенные под ним элементы доступны как данному классу, так и функциям других классов и вообще любым функциям программы через представитель класса.

Есть еще спецификатор защищенного доступа protected, означающий, что элементы в помеченном им разделе доступны не только в данном классе, но и для функций-элементов классов, производных от него.

Структуры, классы и объединения

Типы, определяемые с ключевыми словами struct, class и union, являются классами. Отличия их сводятся к следующему:

Я никогда не видел, чтобы в C++ применяли объединения. Хотя это, возможно, и имело бы смысл в некоторых ситуациях, когда требовалось бы объединить несколько разнородных классов в один тип.

Элементы данных и элементы-функции

Элементы данных класса совершенно аналогичны элементам структур в С, за исключением того, что для них специфицирован определенный тип доступа. Объявления элементов-функций аналогичны прототипам обычных функций.

Конструктор и деструктор

В классе могут быть объявлены две специальные функции-элемента. Это конструктор и деструктор. Класс Line в примере объявляет обе эти функции.

Конструктор отвечает за создание представителей данного класса. Его объявление записывается без типа возвращаемого значения и ничего не возвращает, а имя должно совпадать с именем класса. Конструктор может иметь любые параметры, необходимые для конструирования, т. е. создания, нового представителя класса. Если конструктор не определен, компилятор генерирует конструктор по умолчанию, который просто выделяет память, необходимую для размещения представителя класса.

Деструктор отвечает за уничтожение представителей класса. Это происходит либо в случае, когда автоматический объект данного класса выходит из области действия, либо при удалении динамических объектов, созданных операцией new.

Деструктор объявляется без типа возвращаемого значения, ничего не возвращает и не имеет параметров. Если деструктор не определен, генерируется деструктор по умолчанию, который просто возвращает системе занимаемую объектом память.

Заключение

В этой главе мы постарались познакомить вас, весьма конспективно, с основными понятиями и терминологией объектно-ориентированного программирования. После этого мы перешли к элементарному введению в классы C++. В следующей главе мы продолжим изучение классов, уже на более серьезном уровне.